"""
HTML generation from files templates.
Designed for python developpers who want to have a very light templating system for their HTML pages generation.
It can be used for non html texts.

A sample is provided with cherryPy.
Created on the idea of PEAR PHP template ITX.

This file is provided under GPL (http://www.gnu.org/copyleft/gpl.html).
Have a look on the projects page of www.jmbc.fr

Quick start
===========

Let's consider to have the following text generated ::
	<html>
	<body>
	This is a simple text : hello world !
	</body>
	</html>
We can do that by creating a text file (for instance tutorial.tpl) containing ::
	<html>
	<body>
	<!-- BEGIN TEXT -->This is a simple text : {text}<!-- END TEXT -->
	</body>
	</html>
and a script file (let's say tutorial.py) containing ::
	from balloonHTMLtemplate import *
	tpl= balloonHTMLtemplate('tutorial.tpl')
	tpl.setVar('TEXT','text','hello world !')
	print tpl.render()
and running it : python tutorial.py

How does it work ? The first line just imports the right module (balloonHTMLtemplate), the second one create a template object (tpl), next we feed the variable 'text' with the value 'hello world !' in the block 'TEXT', and finally we print the result (of course we could do anything else with it). The TEXT block is defined inside the template file (tutorial.tpl) by the couple <!-- BEGIN TEXT -->...<!-- END TEXT --> which means all is in place of the three points is view as the TEXT block content. Inside the TEXT block, we have a variable {text} defined by the parenthesis and a name. That means we can replace {text} by all that we want thanks to the setVar method of the template object (tpl). Finally we get the resulting text by the render method of the template object.

It becomes interesting if we have to loop on rows. For instance, we want to get the following text ::
	<html>
	<body>
	<table>
	<tr><td>jean</td><td>dupond</td></tr>
	<tr><td>john</td><td>smith</td></tr>
	<tr><td>juan</td><td>cazillas</td></tr>
	</table>
	</body>
	</html>
We can build the template file ::
	<html>
	<body>
	<!-- BEGIN table --><table>
	<!-- BEGIN tline --><tr><!-- BEGIN tcol --><td>{text}</td><!-- END tcol --></tr>
	<!-- END tline --></table><!-- END table -->
	</body>
	</html>
and get it with the script ::
	from balloonHTMLtemplate import *
	tpl= balloonHTMLtemplate('tutorial.tpl')
	t= (('jean','dupond'),('john','smith'),('juan','cazillas'))
	for l in t:
		for c in l:
			tpl.setVar('tcol','text',c)
			tpl.iterateBlock('tcol')
		tpl.iterateBlock('tline')
	print tpl.render()

What is new is the iterateBlock method. It gives the ability to duplicate a block in a template. All the underlying blocks are also duplicated, and the variables contents cleared. The table block is not really useful, but can be used for instance if we don't need the block ; so we can delete it by tpl.delBlockItem('table'). In that case, the block content is lost.

There are some other features, so feel free to have a look in the demo files and in the following documentation.
"""
#-------------------------------------------------------------------------------
import copy,types,os
if os.name == 'nt':
	import sys, time#pywin.debugger
	#pywin.debugger.set_trace()
#-------------------------------------------------------------------------------
class balloonHTMLtemplate:
	"""
	The main class to instantiate, for instance ::
		tpl1= balloonHTMLtemplate('demo1.tpl')

	The template file can contain any text.
	The templating system recognizes the following definition tags ::
		<!-- BEGIN block_name --> ...any text... {variable_name} <!-- END block_name -->
		<!-- MACRO macro_name --> ...any text... {variable_name} <!-- END macro_name -->

	The names block_name and macro_name must be identicals both at the beginning and the end of the tag.
	Blocks and Macros can be embedded.
	A tag can contain any text and any variables.
	The variable names are case sensitive.
	The blocks are always included in the rendered string, except if explicitely deleted.
	The macros are never included in the rendered string.

	List of the public methods (all private methods are useless for the user):
		- L{__init__}
		- L{setDebug}
		- L{delBlockItem}
		- L{iterateBlock}
		- L{setVarList}
		- L{setVar}
		- L{getMacro}
		- L{render}
		- L{dump}

	See demo.py and demo2.py for sample uses.
	NB : This project is so light that I decided to not provide a setup script.
	"""
	#---------------------------------------------------------------------------
	def __init__(self,path):
		"""
		@param path: Any valid path to the template file.
		@type path: String
		@return: Nothing.
		"""
		self.debug= False
		self.path= path
		#--load template
		file= open(path,'rb')
		self.data= file.read()
		file.close()
		#--
		self.currentBlockName= ''
		self.blockRefs= {}
		self.blocks= self.parseString(self.data)
		self.toIterBlock= None
		self.setVars= {}
		self.gettingMacro= False
	#---------------------------------------------------------------------------
	def setDebug(self):
		"""
		If this method is used, some information is printed while operating.
		@return: Nothing.
		"""
		self.debug= True
	#---------------------------------------------------------------------------
	def parseString(self,tplString):
		"""
		Private method : Creates the blocks definitions at the template initialization.
		Recursive method.

		@param tplString: The template file data.
		@type tplString: String
		@return: A list of blocks.
		"""
		BEGIN_TAG= '<!-- BEGIN '
		MACRO_TAG= '<!-- MACRO '
		END_TAG= '<!-- END '
		TAG_TERM= ' -->'
		#--
		blocks= []
		tplStringU= tplString.upper()
		startPos= 0
		macro= False
		while startPos < len(tplString):
			#--looks for a tag beginning
			bPos= tplStringU.find(BEGIN_TAG,startPos)
			if bPos == -1:
				bPos= tplStringU.find(MACRO_TAG,startPos)
				macro= True
			if bPos == -1:
				#--records the remaining text
				s= tplString[startPos:]
				self.parseUltimateString(self.currentBlockName,s,blocks)
				#--setup end of loop
				startPos= len(tplString)
			else:
				#--looks for tag name
				w= bPos+len(BEGIN_TAG)
				tPos= tplStringU.find(TAG_TERM,w)
				if tPos != -1:
					blockName= tplString[w:tPos]
					#--looks for tag termination
					w= w+len(TAG_TERM)
					ebPos= tplStringU.find(END_TAG+blockName.upper()+TAG_TERM,w)
					if ebPos != -1:
						#--records the text until the block
						s= tplString[startPos:bPos]
						self.parseUltimateString(self.currentBlockName,s,blocks)
						#--parse found block
						self.currentBlockName= blockName
						i= tPos+len(TAG_TERM)
						s= tplString[i:ebPos]
						blockStyle= 'block'
						if macro:
							blockStyle= 'macro'
						blockDef= [blockStyle,blockName,self.parseString(s)]
						blocks.append(blockDef)
						self.blockRefs[blockName]= copy.deepcopy(blocks[-1:])
					#--setup the next search after the begin tag
					startPos= ebPos + len(END_TAG) + len(blockName) + len(TAG_TERM)
		return blocks
	#---------------------------------------------------------------------------
	def parseUltimateString(self,blockName,varString,blocks):
		"""
		Private method : Parses the last part of a block.
		Recursive method.

		@param blockName: The name of the block. Currently not used.
		@type blockName: String
		@param varString: The string to parse.
		@type varString: String
		@param blocks: The blocks structure to append.
		@type blocks: List
		@return: Nothing.
		"""
		startPos= 0
		while startPos < len(varString):
			#--looks for a variable definition
			i= varString.find('{',startPos)
			if i == -1:
				#--records the remaining text
				blockDef= ['text',None,varString[startPos:]]
				if len(blockDef[2]) > 0:
					blocks.append(blockDef)
				#--setup end of loop
				startPos= len(varString)
			else:
				j= varString.find('}',i)
				if j == -1:
					#--would raise something, but accepts var def without trailing }
					j= len(varString)
				#--records the text until the definition
				blockDef= ['text',None,varString[startPos:i]]
				if len(blockDef[2]) > 0:
					blocks.append(blockDef)
				#--records the var definition
				varName= varString[i+1:j]
				blockDef= ['var',varName,'']
				if len(blockDef[1]) > 0:
					blocks.append(blockDef)
				#--setup the next search after the variable name
				startPos= j+1
	#---------------------------------------------------------------------------
	def delBlockItem(self,blockName):
		"""
		Deletes a block definition, ie the deleted block won't appear in the rendered string.

		@param blockName: The name of the block to delete.
		@type tplString: String
		@return: Nothing.
		"""
		self.delDone= False
		self.doDelBlockItem(self.blocks,blockName)
	#---------------------------------------------------------------------------
	def doDelBlockItem(self,block,blockName):
		"""
		Private method : Block deletion.
		Recursive method.

		@param block: The blocks structure to update.
		@type block: List
		@param blockName: The name of the block to delete.
		@type blockName: String
		@return: Nothing.
		"""
		i= len(block)
		while not self.delDone and i >= 0:
			i-= 1
			if block[i][1] == blockName:
				del block[i]
				self.delDone= True
			elif block[i][0] == 'block':
				self.doDelBlockItem(block[i][2],blockName)
	#---------------------------------------------------------------------------
	def iterateBlock(self,blockName):
		"""
		Block iteration.
		The templating system memorizes the block to iterate. That means if the block
		is not used later it won't be iterated. The real iteration will occurs when a
		variable in the block will be set.
		At the real iteration, the iterated block will be repeated one more time in the blocks structure.
		It takes in account the sub-blocks within the block scope.
		The new block variables will be reset.

		@param blockName: The name of the block to iterate.
		@type blockName: String
		@return: Nothing.
		"""
		if self.debug:
			print 'trying to iterate %s'%blockName
		self.toIterBlock= blockName
	#---------------------------------------------------------------------------
	def doIterateBlock(self,blockName):
		"""
		Private method : Real iteration method.

		@param blockName: The name of the block to really iterate.
		@type blockName: String
		@return: Nothing.
		"""
		if self.toIterBlock != None and self.setVars.has_key(blockName):
			self.iterDone= False
			self.iterBlock(self.blocks,self.toIterBlock)
		self.toIterBlock= None
	#---------------------------------------------------------------------------
	def iterBlock(self,block,blockName):
		"""
		Private method : Block iteration.
		Recursive method.

		@param block: The blocks structure to update.
		@type block: List
		@param blockName: The name of the block to delete.
		@type blockName: String
		@return: Nothing.
		"""
		i= len(block)
		while not self.iterDone and i >= 0:
			i-= 1
			if block[i][0] == 'block' and block[i][1] == blockName:
				if self.debug:
					print 'iterating %s'%blockName
				#--copy the fresh version
				block[i+1:i+1]= copy.deepcopy(self.blockRefs[blockName])
				self.iterDone= True
				if self.setVars.has_key(blockName):
					del self.setVars[blockName]
			elif block[i][0] == 'block':
				#--look in this one up
				self.iterBlock(block[i][2],blockName)
	#---------------------------------------------------------------------------
	def setVarList(self,blockNames,varName,data,filter=None):
		"""
		Sets a list of values within a list of blocks for a variable.
		The calling program can provide a filtering method.
		The blocks deepth is currently limited to two.
		For instance ::
			texts= [['name a1','name a2','name a3'],['name b1','name b2','name b3']]
			tpl.setVarList(['tline','tcol'],'text',texts,self.filter)

		@param blockNames: The block name, or a list of two block names.
		@type blockNames: String or List
		@param varName: The variable name.
		@type varName: String
		@param data: Values.
		@type data: List
		@param filter: An external filtering method. Will receive a row number and a string value, and must return a string value.
		@type filter: Method
		@return: Nothing.
		"""
		if type(blockNames) is types.ListType:
			for row in data:
				for i,val in enumerate(row):
					if filter != None:
						val= filter(i,val)
					self.setVar(blockNames[1],varName,val)
					self.iterateBlock(blockNames[1])
				self.iterateBlock(blockNames[0])
		else:
			for i,val in enumerate(data):
				if filter != None:
					val= filter(i,val)
				self.setVar(blockNames,varName,val)
				self.iterateBlock(blockNames)
	#---------------------------------------------------------------------------
	def setVar(self,blockName,varName,value=''):
		"""
		Sets a value within a block for a variable.

		@param blockName: The block name.
		@type blockName: String
		@param varName: The variable name.
		@type varName: String
		@param value: Value (default= an empty string).
		@type value: String
		@return: Nothing.
		"""
		if not self.gettingMacro:
			self.doIterateBlock(blockName)
		try:
			val= str(value).decode('utf8').encode('iso-8859-1')
		except UnicodeDecodeError:
			val= str(value)
		self.setDone= False
		self.currentBlockName= ''
		self.setV(self.blocks,blockName,varName,val)
		if not self.setDone and self.debug:
			print 'variable "%s" not found in block "%s"'%(varName,blockName)
	#---------------------------------------------------------------------------
	def setV(self,block,blockName,varName,value):
		"""
		Private method : Sets a value within a block for a variable.
		Recursive method.

		@param blockName: The block name.
		@type blockName: String
		@param varName: The variable name.
		@type varName: String
		@param value: Value (default= an empty string).
		@type value: String
		@return: Nothing.
		"""
		i= len(block)
		while not self.setDone and i >= 0:
			i-= 1
			if block[i][0] == 'var' and self.currentBlockName == blockName and block[i][1] == varName:
				if self.debug:
					print 'setting %s to "%s" in %s'%(varName,value,blockName)
				#--sets the value
				block[i][2]= value
				self.setDone= True
			elif block[i][0] == 'block' or block[i][0] == 'macro':
				#--find in this one
				tmp= self.currentBlockName
				self.currentBlockName= block[i][1]
				self.setV(block[i][2],blockName,varName,value)
				if self.setDone == True:
					self.setVars[blockName]= True
				self.currentBlockName= tmp
	#---------------------------------------------------------------------------
	def getMacro(self,macroName,valueDict={}):
		"""
		Gets a string value from a template macro giving values for variables macro.
		For instance ::
			text= tpl2.getMacro('LINK',{'url':'localhost','urltext':'cliquez ici'})

		@param macroName: The macro name.
		@type macroName: String
		@param valueDict: A list of variable names and the associated values (default= empty).
		@type valueDict: Dictionnary
		@return: A string value.
		"""
		self.gettingMacro= True
		txt= ''
		for varType,name,val in self.blocks:
			if varType == 'macro' and name == macroName:
				for var in valueDict:
					self.setVar(macroName,var,value=valueDict[var])
				txt= self.renderBlock(val)
		self.gettingMacro= False
		return txt
	#---------------------------------------------------------------------------
	def render(self):
		"""
		Returns a string with the text finally given by the template engine.

		@return: A string value.
		"""
		if self.debug:
			self.dump()
		return self.renderBlock(self.blocks)
	#---------------------------------------------------------------------------
	def renderBlock(self,block):
		"""
		Private method : Rendering method.
		Recursive method.

		@param block: A block structure.
		@type block: List
		@return: A string value.
		"""
		s= ''
		for varType,name,val in block:
			if varType == 'block':
				s= s+self.renderBlock(val)
			elif varType == 'var':
				s= s+str(val)
			elif varType == 'text':
				s= s+str(val)
		return s
	#---------------------------------------------------------------------------
	def dump(self,txt=''):
		"""
		Output the internal blocks structure in a file. Useful in debug.
		The file name is the same that the template file, with '.dump' appended.

		@param txt: A string to append to the file name (default= empty).
		@type blockName: String
		@return: Nothing.
		"""
		fn= self.path+'.dump'+txt
		print 'dumping template on %s'%fn
		file= open(fn,'w')
		self.dumpBlock(file,self.blocks)
		file.close()
	#---------------------------------------------------------------------------
	def dumpBlock(self,file,block):
		"""
		Private method : Dumping method.
		Recursive method.

		@param file: The file to feed.
		@type file: File object
		@param block: The block structure.
		@type block: List
		@return: Nothing.
		"""
		for varType,name,val in block:
			if varType == 'block':
				file.write('----------block %s\n'%name)
				self.dumpBlock(file,val)
			elif varType == 'macro':
				file.write('----------macro %s\n'%name)
				self.dumpBlock(file,val)
			elif varType == 'var':
				file.write('var %s=[[%s]]\n'%(name,val))
			elif varType == 'text':
				file.write('text=[[%s]]\n'%val)
#-------------------------------------------------------------------------------
